import java.io.*;
import java.sql.Date;
import java.util.*;
import java.math.BigInteger;

import org.joda.time.*;

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.*;

// From http://docs.oracle.com/javase/6/docs/api/java/sql/Date.html, 
// as of Feb 28, 2012:
// -------------------
// Date: A milliseconds value that represents the number of milliseconds that
// have passed since January 1, 1970 00:00:00.000 GMT.  
//
// To conform with the definition of SQL DATE, the millisecond values wrapped
// by a java.sql.Date instance must be 'normalized' by setting the hours,
// minutes, seconds, and milliseconds to zero in the particular time zone with
// which the instance is associated.

public class Scheduler {
    private enum Change { AVAILABLE, UNAVAILABLE }
    private HashMap events;
    private ArrayList<ISchedulable> schedulables;
    private ArrayList<AvailabilityStateChange> stateChanges;
    private ArrayList<ValidEvent> validEvents;
    private LinkedList<EventPossibility> possibleEvents;
    private long time_frame_start;
    private long time_frame_end;

    long len_threshold;

    int mandatoryCount;

    private int countMandatorySchedulables() {
	int count = 0;
	for(ISchedulable s : schedulables)
	    count += s.getPriority();
	return count;
    }

    private class ValidEventComparator implements Comparator<ValidEvent> {
	public int compare(ValidEvent v1, ValidEvent v2) {
	    Integer i_1 = new Integer(v1.schedulables.size());
	    Integer i_2 = new Integer(v2.schedulables.size());
	    return i_2.compareTo(i_1);
	}
    }

    /* Represents an instance of a resource
       e.g. room, projector, etc..*/
    private class Resource implements ISchedulable {
	String id;
	int priority;
	private ArrayList<Event> schedule;

	public int getPriority() {
	    return priority;
	}

	public ArrayList<Event> getSchedule() {
	    return schedule;
	}

	public String getID() {
	    return id;
	}
	
	public int getIID() {
	    return 0;
	}
    

	public Resource(Vector events) {
	    priority = 0;
	}
    }

    /* Javaesqe */
    private class AvailabilityStateChangeComparator implements Comparator<AvailabilityStateChange> {
	public int compare(AvailabilityStateChange c1, AvailabilityStateChange c2) {
	    Long i_1 = Long.valueOf(c1.time);
	    Long i_2 = Long.valueOf(c2.time);
	    return i_1.compareTo(i_2);
	}
    }

    /* A change in state from busy to free or free to busy for a schedulable. */
    private class AvailabilityStateChange {
	public ISchedulable schedulable;
	public Change change;
	public long time;

	public String toString() {
	    String c;
	    if(change == Change.AVAILABLE) c = "+";
	    else c = "-";
	    return schedulable.getID() + c;
	}

	public AvailabilityStateChange(ISchedulable schedulable, Change change, long time) {
	    this.schedulable = schedulable;
	    this.change = change;
	    this.time = time;
	}
    }

    
    /* Produce an AvailabilityStateChange object for each scheduled event
     * under all invitees in the domain of schedulables */
    private void createStateChanges() {
	stateChanges = new ArrayList();
	for(ISchedulable s : schedulables) {
	    for(Event e : s.getSchedule()) {
		stateChanges.add(new AvailabilityStateChange(s, Change.UNAVAILABLE, e.start));
		stateChanges.add(new AvailabilityStateChange(s, Change.AVAILABLE, e.end));
	    }
	}
	Collections.sort(stateChanges, new AvailabilityStateChangeComparator());
    }
    
    /* add to the list of possible events all current possible combinations including s */
    private void updateEventPossibilities(ISchedulable s, long time) {
	LinkedList<EventPossibility> toAdd = new LinkedList<EventPossibility>();
	for(EventPossibility base : possibleEvents) {
	    toAdd.add(new EventPossibility(time, base, s));
	}
	possibleEvents.addAll(toAdd);
	possibleEvents.add(new EventPossibility(time, s));
    }

    /* Pre-populate the list of possible events with a preliminary state-change to free.*/
    private void initializeEventPossibilities() {
	possibleEvents = new LinkedList<EventPossibility>();
	for(ISchedulable s : schedulables) {
	    updateEventPossibilities(s, time_frame_start);
	}
	// Check that everything is added correctly:
	BigInteger expected = TestMath.countOpenEventPossibilities(schedulables.size());
	BigInteger calculated = new BigInteger(Integer.toString(possibleEvents.size()));
	if(!calculated.equals(expected)) {
	    System.err.println("Error with possibility initialization: ");
	    System.err.format("Expected %d, produced %d", expected, possibleEvents.size());
	    System.err.println();
	}
    }

    /* Remove possible events that include schedulables who have just become busy */
    /* return the list of "closed" event possibilities */
    private LinkedList<EventPossibility> closeEventPossibilities(ISchedulable s, long time) {
	LinkedList<EventPossibility> closed = new LinkedList<EventPossibility>();
	LinkedList<EventPossibility> validClosed = new LinkedList<EventPossibility>();
	for(EventPossibility ep : possibleEvents) {
	    if(ep.schedulables.contains(s)) {
		ep.end_time = time;
		closed.add(ep);
	    }
	}
	for(EventPossibility ep : closed) {
	    int index = possibleEvents.indexOf(ep);
	    possibleEvents.remove(index);
	}
	return closed;
    }

    /* Process a state change */
    private void processStateChange(AvailabilityStateChange sc) {
	if(sc.change == Change.AVAILABLE) {
	    updateEventPossibilities(sc.schedulable, sc.time);
	}
	else {
	    LinkedList<EventPossibility> closed = closeEventPossibilities(sc.schedulable, sc.time);
	    for(EventPossibility ep : closed) {
		if(ep.end_time - ep.start_time >= len_threshold && 
		   ep.getMandatoryCount() == mandatoryCount)
		    validEvents.add(new ValidEvent(ep));
	    }
	}
    }

    /* Run the algorithm. */
    private void run() {
	for(AvailabilityStateChange sc : stateChanges) {
	    processStateChange(sc);
	}
	
	for(EventPossibility ep : possibleEvents) {
	    ep.end_time = time_frame_end;
	    if(ep.end_time - ep.start_time >= len_threshold &&
	       ep.getMandatoryCount() == mandatoryCount) // Place threshhold here..
		    validEvents.add(new ValidEvent(ep));
	}

	Collections.sort(validEvents, new ValidEventComparator());
    }
    
    void printPossibleEventList() {
	for(EventPossibility e : possibleEvents) {
	    System.out.format("(");
	    for(ISchedulable s : e.schedulables) {
		System.out.format("%s ", s.getID());
	    }
	    System.out.print(") ");
	}
	System.out.println();
    }

    void printStateChanges() {
	for(AvailabilityStateChange sc : stateChanges) {
	    System.out.print("[" + sc + sc.time + "] ,");
	}
	System.out.println();
    }

    void printValidEventList() {
	for(ValidEvent ve : validEvents) {
	    DateTime start = new DateTime(ve.start);
	    DateTime stop = new DateTime(ve.end);
	    System.out.format("[(%s-%s) ", start.toString(), stop.toString());
	    for(ISchedulable s : ve.schedulables) {
		System.out.format("%s ", s.getID());
	    }    
	    System.out.println("]");
	}
    }

    public ValidEvent getBestValidEvent() {
	if(validEvents.isEmpty()) return null;
	return validEvents.get(0);
    }

    public void cleanEvents(HashMap<String, Event> events, NewEvent nv) {
	long start = nv.getStartMS();
	long stop = nv.getStopMS();
	LinkedList<String> rmList = new LinkedList<String>();
	for(String k : events.keySet()) {
	    Event ev = events.get(k);
	    if(ev.end < start || ev.start > stop || ev.start >= ev.end) {
		rmList.add(k);
		continue;
	    }
	    if(ev.start < start) ev.start = start;
	    if(ev.end > stop) ev.end = stop;
	}
	for(String k : rmList) {
	    events.remove(k);
	}
    }

    public Scheduler(int new_event_ID, ScheduleProDataConnection sproData) {  // This will eventually take as input the new event key...
	try {
	    sproData.putSchedulerMessage(new_event_ID, "Finding events...");

	    events = new HashMap<String, Event>();
	    schedulables = new ArrayList<ISchedulable>();

	    NewEvent newEvent = sproData.getNewEventEntry(new_event_ID);

	    events.putAll(sproData.getInviteeEvents(new_event_ID));
	    events.putAll(sproData.getInviteePrivateEvents(new_event_ID));

	    cleanEvents(events, newEvent);

	    schedulables.addAll(sproData.getInvitees(new_event_ID, events));

	    len_threshold = newEvent.getLength(); // Convert hours to ms...
	    
	    time_frame_start = newEvent.getStart().getMillis();
	    time_frame_end = newEvent.getStop().getMillis();

	    initializeEventPossibilities();
	    createStateChanges();

	    validEvents = new ArrayList<ValidEvent>();

	    mandatoryCount = countMandatorySchedulables();

	    //printPossibleEventList(); // Dump initial states to stdout
	    //printStateChanges();

	    run();
	    
	    if(validEvents.isEmpty())
		sproData.putSchedulerMessage(new_event_ID, "No events found!");
	    else {
		sproData.putSchedulerMessage(new_event_ID, "Successfully found an event!");
		ValidEvent best = validEvents.get(0);
		best.end = best.start + len_threshold;
		sproData.putValidEvent(best, newEvent);
	    }
	}
	catch(Exception e) {
	    System.err.println("In " + this.getClass().getName() + " constructor:");
	    System.err.println("Error: " + e.getMessage());
	}
    }

    public static void main(String [] args) {
	if(args.length > 1 && args[1].equals("RUNTEST")) {
	    Result result = JUnitCore.runClasses(TestScheduleProDataConnection.class);
	    boolean failed = false;
	    int i = 0;
	    if(result.getFailures().size() > 0)
		failed = true;
	    for(Failure f : result.getFailures()) {
		System.out.println((++i) + "\t: " + f.toString());
	    }
	    //System.out.println("Done running test.");

	    if(failed) {
		System.err.println("Failed one or more tests!");
	    }
	}
	else {
	    if(args.length < 1) {
		System.err.println("Usage: Scheduler <new_event_ID>");
	    }
	    else {
		//System.out.format("Solving for %s\n", args[0]);
		ScheduleProDataConnection sproData = new ScheduleProDataConnection
		    ("jdbc:mysql://411green.cs.odu.edu:3306/schedulepro",
		     "411green",
		     "fpjcsa");
		Scheduler scheduler = new Scheduler(Integer.parseInt(args[0]), sproData);
		sproData.close();
		//scheduler.printValidEventList();
	    }
	}
    }
}
